สำรวจทางเลือก enum ใน TypeScript เช่น const assertions และ union types ทำความเข้าใจประโยชน์ ข้อเสีย และการใช้งานจริง เพื่อโค้ดที่สะอาด บำรุงรักษาง่ายขึ้น ในบริบทการพัฒนาทั่วโลก
ทางเลือก TypeScript Enum: การสำรวจ Const Assertions และ Union Types เพื่อโค้ดที่แข็งแกร่ง
TypeScript ซึ่งเป็น superset ที่ทรงพลังของ JavaScript ได้นำการระบุชนิดข้อมูลแบบคงที่ (static typing) มาสู่โลกของการพัฒนาเว็บแบบไดนามิก ในบรรดาฟีเจอร์มากมาย คำหลัก enum เป็นที่นิยมใช้ในการกำหนดชุดของค่าคงที่ที่มีชื่อ Enums มอบวิธีการที่ชัดเจนในการแสดงชุดของค่าที่เกี่ยวข้องแบบคงที่ ซึ่งช่วยเพิ่มความสามารถในการอ่านโค้ดและความปลอดภัยของชนิดข้อมูล
อย่างไรก็ตาม เมื่อระบบนิเวศของ TypeScript เติบโตขึ้น และโปรเจกต์มีความซับซ้อนและขนาดใหญ่ขึ้น นักพัฒนาทั่วโลกเริ่มตั้งคำถามถึงประโยชน์ดั้งเดิมของ enums แม้จะใช้งานง่ายสำหรับกรณีทั่วไป แต่ enums มีพฤติกรรมและลักษณะเฉพาะที่รันไทม์ซึ่งบางครั้งอาจนำไปสู่ปัญหาที่ไม่คาดคิด ส่งผลต่อขนาดของแพ็คเกจ (bundle size) หรือทำให้การปรับแต่ง tree-shaking ซับซ้อนขึ้น สิ่งนี้ทำให้เกิดการสำรวจทางเลือกอื่นอย่างกว้างขวาง
คู่มือฉบับสมบูรณ์นี้จะเจาะลึกถึงสองทางเลือกที่โดดเด่นและมีประสิทธิภาพสูงสำหรับ TypeScript enums ได้แก่ Union Types กับ String/Numeric Literals และ Const Assertions (as const) เราจะสำรวจกลไก การใช้งานจริง ประโยชน์ และข้อเสียของสิ่งเหล่านี้ โดยให้ความรู้แก่คุณในการตัดสินใจออกแบบอย่างรอบรู้สำหรับโปรเจกต์ของคุณ ไม่ว่าจะมีขนาดเท่าใด หรือทีมทั่วโลกที่ทำงานร่วมกัน เป้าหมายของเราคือการช่วยให้คุณเขียนโค้ด TypeScript ที่แข็งแกร่ง บำรุงรักษาง่าย และมีประสิทธิภาพมากขึ้น
TypeScript Enum: สรุปโดยย่อ
ก่อนที่เราจะเจาะลึกถึงทางเลือกอื่น ๆ มาทบทวน enum แบบดั้งเดิมของ TypeScript โดยย่อกันก่อน Enums ช่วยให้นักพัฒนาสามารถกำหนดชุดของค่าคงที่ที่มีชื่อ ทำให้โค้ดอ่านง่ายขึ้น และป้องกันการใช้ "magic strings" หรือ "magic numbers" กระจัดกระจายไปทั่วแอปพลิเคชัน Enums มีสองรูปแบบหลักคือ numeric และ string enums
Numeric Enums
โดยค่าเริ่มต้น TypeScript enums เป็นแบบตัวเลข สมาชิกตัวแรกจะถูกกำหนดค่าเริ่มต้นด้วย 0 และสมาชิกที่ตามมาแต่ละตัวจะเพิ่มค่าขึ้นเองโดยอัตโนมัติ
enum Direction {
Up,
Down,
Left,
Right,
}
let currentDirection: Direction = Direction.Up;
console.log(currentDirection); // Outputs: 0
console.log(Direction.Left); // Outputs: 2
คุณยังสามารถกำหนดค่าเริ่มต้นให้กับสมาชิก enum แบบตัวเลขได้ด้วยตนเอง:
enum StatusCode {
Success = 200,
NotFound = 404,
ServerError = 500,
}
let status: StatusCode = StatusCode.NotFound;
console.log(status); // Outputs: 404
ลักษณะเฉพาะอย่างหนึ่งของ numeric enums คือ reverse mapping ที่รันไทม์ numeric enum จะถูกคอมไพล์เป็นออบเจกต์ JavaScript ที่แมปทั้งชื่อกับค่า และค่ากลับไปยังชื่อ
enum UserRole {
Admin = 1,
Editor,
Viewer,
}
console.log(UserRole[1]); // Outputs: "Admin"
console.log(UserRole.Editor); // Outputs: 2
console.log(UserRole[2]); // Outputs: "Editor"
/*
Compiles to JavaScript:
var UserRole;
(function (UserRole) {
UserRole[UserRole["Admin"] = 1] = "Admin";
UserRole[UserRole["Editor"] = 2] = "Editor";
UserRole[UserRole["Viewer"] = 3] = "Viewer";
})(UserRole || (UserRole = {}));
*/
String Enums
String enums มักถูกนิยมใช้เพราะอ่านง่ายที่รันไทม์ เนื่องจากไม่ขึ้นอยู่กับตัวเลขที่เพิ่มขึ้นอัตโนมัติ สมาชิกแต่ละตัวต้องถูกกำหนดค่าเริ่มต้นด้วย string literal
enum UserPermission {
Read = "READ_PERMISSION",
Write = "WRITE_PERMISSION",
Delete = "DELETE_PERMISSION",
}
let permission: UserPermission = UserPermission.Write;
console.log(permission); // Outputs: "WRITE_PERMISSION"
String enums ไม่มี reverse mapping ซึ่งโดยทั่วไปเป็นสิ่งที่ดีสำหรับการหลีกเลี่ยงพฤติกรรมที่ไม่คาดคิดที่รันไทม์และลดเอาต์พุต JavaScript ที่สร้างขึ้น
ข้อควรพิจารณาที่สำคัญและข้อผิดพลาดที่อาจเกิดขึ้นของ Enums
แม้ว่า enums จะให้ความสะดวกสบาย แต่ก็มาพร้อมกับคุณลักษณะบางอย่างที่ต้องพิจารณาอย่างรอบคอบ:
- ออบเจกต์รันไทม์: ทั้ง numeric และ string enums สร้างออบเจกต์ JavaScript ที่รันไทม์ ซึ่งหมายความว่ามันเพิ่มขนาดของแพ็คเกจแอปพลิเคชันของคุณ แม้ว่าคุณจะใช้มันสำหรับการตรวจสอบชนิดข้อมูลเท่านั้น สำหรับโปรเจกต์ขนาดเล็ก อาจเป็นเรื่องเล็กน้อย แต่ในแอปพลิเคชันขนาดใหญ่ที่มี enums จำนวนมาก ก็อาจเพิ่มขึ้นได้
- ขาด Tree-Shaking: เนื่องจาก enums เป็นออบเจกต์รันไทม์ จึงมักไม่ถูก tree-shake ได้อย่างมีประสิทธิภาพโดย bundler สมัยใหม่ เช่น Webpack หรือ Rollup หากคุณกำหนด enum แต่ใช้สมาชิกเพียงหนึ่งหรือสองตัวเท่านั้น ออบเจกต์ enum ทั้งหมดก็ยังคงอาจถูกรวมอยู่ในแพ็คเกจสุดท้ายของคุณ ซึ่งอาจนำไปสู่ขนาดไฟล์ที่ใหญ่เกินความจำเป็น
- Reverse Mapping (Numeric Enums): คุณสมบัติ reverse mapping ของ numeric enums แม้ว่าบางครั้งจะมีประโยชน์ แต่ก็อาจเป็นสาเหตุของความสับสนและพฤติกรรมที่ไม่คาดคิดได้ มันเพิ่มโค้ดพิเศษให้กับเอาต์พุต JavaScript และอาจไม่ใช่ฟังก์ชันการทำงานที่ต้องการเสมอไป ตัวอย่างเช่น การ serialize numeric enums บางครั้งอาจนำไปสู่การจัดเก็บเพียงแค่ตัวเลข ซึ่งอาจไม่ละเอียดเท่ากับสตริง
- ค่าใช้จ่ายในการ Transpilation: การคอมไพล์ enums เป็นออบเจกต์ JavaScript เพิ่มค่าใช้จ่ายเล็กน้อยให้กับกระบวนการบิลด์ เมื่อเทียบกับการกำหนดตัวแปรคงที่แบบธรรมดา
- การวนซ้ำที่จำกัด: การวนซ้ำค่า enum โดยตรงอาจไม่ใช่เรื่องง่าย โดยเฉพาะอย่างยิ่งกับ numeric enums เนื่องจาก reverse mapping คุณมักจะต้องใช้ฟังก์ชันช่วยเหลือหรือลูปเฉพาะเพื่อดึงค่าที่ต้องการเท่านั้น
ประเด็นเหล่านี้เน้นย้ำว่าทำไมทีมพัฒนาทั่วโลกจำนวนมาก โดยเฉพาะอย่างยิ่งผู้ที่เน้นประสิทธิภาพและขนาดของแพ็คเกจ จึงกำลังมองหาทางเลือกอื่นที่ให้ความปลอดภัยของชนิดข้อมูลที่คล้ายกัน โดยไม่มีภาระที่รันไทม์หรือความซับซ้อนอื่น ๆ
ทางเลือกที่ 1: Union Types กับ Literals
หนึ่งในทางเลือกที่ตรงไปตรงมาและทรงพลังที่สุดสำหรับ enums ใน TypeScript คือการใช้ Union Types กับ String หรือ Numeric Literals วิธีนี้ใช้ประโยชน์จากระบบชนิดข้อมูลที่แข็งแกร่งของ TypeScript เพื่อกำหนดชุดของค่าที่อนุญาตที่เฉพาะเจาะจง ณ คอมไพล์ไทม์ โดยไม่ต้องแนะนำโครงสร้างใหม่ใดๆ ที่รันไทม์
Union Types คืออะไร?
union type อธิบายค่าที่สามารถเป็นหนึ่งในหลายชนิดข้อมูล ตัวอย่างเช่น string | number หมายความว่าตัวแปรสามารถเก็บได้ทั้งสตริงหรือตัวเลข เมื่อรวมกับ literal types (เช่น "success", 404) คุณสามารถกำหนดชนิดข้อมูลที่สามารถเก็บชุดของค่าที่กำหนดไว้ล่วงหน้าเท่านั้น
ตัวอย่างการใช้งานจริง: การกำหนดสถานะด้วย Union Types
ลองพิจารณาสถานการณ์ทั่วไป: การกำหนดชุดสถานะที่เป็นไปได้สำหรับงานประมวลผลข้อมูลหรือบัญชีผู้ใช้ ด้วย union types วิธีนี้จะดูสะอาดและกระชับ:
type JobStatus = "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
function processJob(status: JobStatus): void {
if (status === "COMPLETED") {
console.log("Job finished successfully.");
} else if (status === "FAILED") {
console.log("Job encountered an error.");
} else {
console.log(`Job is currently ${status}.`);
}
}
let currentJobStatus: JobStatus = "IN_PROGRESS";
processJob(currentJobStatus);
// This would result in a compile-time error:
// let invalidStatus: JobStatus = "CANCELLED"; // Error: Type '"CANCELLED"' is not assignable to type 'JobStatus'.
สำหรับค่าตัวเลข รูปแบบจะเหมือนกัน:
type HttpCode = 200 | 400 | 404 | 500;
function handleResponse(code: HttpCode): void {
if (code === 200) {
console.log("Operation successful.");
} else if (code === 404) {
console.log("Resource not found.");
}
}
let responseStatus: HttpCode = 200;
handleResponse(responseStatus);
โปรดสังเกตว่าเรากำลังกำหนด type alias ที่นี่ นี่เป็นโครงสร้างที่ทำงานเฉพาะตอนคอมไพล์ไทม์เท่านั้น เมื่อคอมไพล์เป็น JavaScript แล้ว JobStatus จะหายไป และ literal strings/numbers จะถูกนำมาใช้โดยตรง
ประโยชน์ของ Union Types กับ Literals
วิธีนี้มีข้อดีหลายประการที่น่าสนใจ:
- ทำงานเฉพาะตอนคอมไพล์ไทม์: Union types จะถูกลบออกไปทั้งหมดในระหว่างการคอมไพล์ โดยไม่สร้างโค้ด JavaScript ใดๆ ที่รันไทม์ ซึ่งนำไปสู่ขนาดแพ็คเกจที่เล็กลงและเวลาเริ่มต้นแอปพลิเคชันที่เร็วขึ้น นี่เป็นข้อได้เปรียบที่สำคัญสำหรับแอปพลิเคชันที่เน้นประสิทธิภาพ และแอปพลิเคชันที่ใช้งานทั่วโลก ซึ่งทุกกิโลไบต์มีความสำคัญ
- ความปลอดภัยของชนิดข้อมูลที่ยอดเยี่ยม: TypeScript ตรวจสอบการกำหนดค่าอย่างเข้มงวดกับชนิด literal ที่กำหนดไว้ ให้การรับประกันที่แข็งแกร่งว่าใช้เฉพาะค่าที่ถูกต้องเท่านั้น ซึ่งป้องกันข้อผิดพลาดทั่วไปที่เกี่ยวข้องกับการพิมพ์ผิดหรือค่าที่ไม่ถูกต้อง
- Tree-Shaking ที่เหมาะสมที่สุด: เนื่องจากไม่มีออบเจกต์รันไทม์ union types จึงรองรับ tree-shaking โดยธรรมชาติ Bundler ของคุณจะรวมเฉพาะ string หรือ numeric literals ที่คุณใช้จริงเท่านั้น ไม่ใช่ออบเจกต์ทั้งหมด
- ความสามารถในการอ่าน: สำหรับชุดค่าที่เรียบง่ายและแตกต่างกัน การนิยามชนิดข้อมูลมักจะชัดเจนและเข้าใจง่ายมาก
- ความเรียบง่าย: ไม่มีการแนะนำโครงสร้างภาษาใหม่ หรือสิ่งประดิษฐ์การคอมไพล์ที่ซับซ้อน มันเป็นเพียงการใช้ประโยชน์จากคุณสมบัติชนิดข้อมูลพื้นฐานของ TypeScript
- การเข้าถึงค่าโดยตรง: คุณทำงานกับค่า string หรือ number โดยตรง ซึ่งช่วยลดความซับซ้อนของการ serialize และ deserialize โดยเฉพาะอย่างยิ่งเมื่อโต้ตอบกับ API หรือฐานข้อมูลที่ต้องการตัวระบุสตริงที่เฉพาะเจาะจง
ข้อเสียของ Union Types กับ Literals
แม้ว่าจะมีประสิทธิภาพ แต่ union types ก็มีข้อจำกัดบางประการ:
- การซ้ำซ้อนสำหรับข้อมูลที่เกี่ยวข้อง: หากคุณต้องการเชื่อมโยงข้อมูลเพิ่มเติมหรือเมตาดาต้ากับสมาชิก "enum" แต่ละตัว (เช่น ป้ายกำกับสำหรับแสดงผล, ไอคอน, สี) คุณไม่สามารถทำได้โดยตรงภายในคำนิยาม union type คุณจะต้องใช้ออบเจกต์การแมปแยกต่างหาก
- ไม่มีการวนซ้ำค่าทั้งหมดโดยตรง: ไม่มีวิธีในตัวที่จะรับอาร์เรย์ของค่าที่เป็นไปได้ทั้งหมดจาก union type ที่รันไทม์ ตัวอย่างเช่น คุณไม่สามารถรับ
["PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"]โดยตรงจากJobStatusได้ง่ายๆ ซึ่งมักจะต้องรักษาอาร์เรย์ของค่าแยกต่างหาก หากคุณต้องการแสดงใน UI (เช่น เมนูดรอปดาวน์) - รวมศูนย์น้อยลง: หากชุดของค่าจำเป็นทั้งในฐานะชนิดข้อมูลและในฐานะอาร์เรย์ของค่ารันไทม์ คุณอาจพบว่าตัวเองต้องกำหนดรายการสองครั้ง (ครั้งหนึ่งเป็นชนิดข้อมูล อีกครั้งเป็นอาร์เรย์รันไทม์) ซึ่งอาจนำไปสู่ความไม่ตรงกันได้
แม้จะมีข้อเสียเหล่านี้ แต่สำหรับสถานการณ์หลายอย่าง union types ก็เป็นทางออกที่สะอาด มีประสิทธิภาพ และปลอดภัยด้านชนิดข้อมูล ซึ่งสอดคล้องกับแนวทางการพัฒนา JavaScript สมัยใหม่เป็นอย่างดี
ทางเลือกที่ 2: Const Assertions (as const)
การใช้ as const ซึ่งเปิดตัวใน TypeScript 3.4 เป็นอีกหนึ่งเครื่องมือที่ทรงพลังอย่างยิ่งที่นำเสนอทางเลือกที่ยอดเยี่ยมแทน enums โดยเฉพาะอย่างยิ่งเมื่อคุณต้องการออบเจกต์รันไทม์ และ การอนุมานชนิดข้อมูลที่แข็งแกร่ง มันช่วยให้ TypeScript อนุมานชนิดข้อมูลที่แคบที่สุดเท่าที่จะเป็นไปได้สำหรับ literal expressions
Const Assertions คืออะไร?
เมื่อคุณใช้ as const กับตัวแปร อาร์เรย์ หรือ object literal, TypeScript จะถือว่าคุณสมบัติทั้งหมดภายใน literal นั้นเป็น readonly และอนุมานชนิดข้อมูลที่เป็น literal ของคุณสมบัติเหล่านั้น แทนที่จะเป็นชนิดข้อมูลที่กว้างกว่า (เช่น "foo" แทนที่จะเป็น string, 123 แทนที่จะเป็น number) สิ่งนี้ทำให้สามารถสร้าง union types ที่เฉพาะเจาะจงสูงจากโครงสร้างข้อมูลรันไทม์ได้
ตัวอย่างการใช้งานจริง: การสร้างออบเจกต์ "Pseudo-Enum" ด้วย as const
มาทบทวนตัวอย่างสถานะงานของเราอีกครั้ง ด้วย as const เราสามารถกำหนดแหล่งที่มาเดียวสำหรับสถานะของเรา ซึ่งทำหน้าที่เป็นทั้งออบเจกต์รันไทม์และพื้นฐานสำหรับการนิยามชนิดข้อมูล
const JobStatuses = {
PENDING: "PENDING",
IN_PROGRESS: "IN_PROGRESS",
COMPLETED: "COMPLETED",
FAILED: "FAILED",
} as const;
// JobStatuses.PENDING is now inferred as type "PENDING" (not just string)
// JobStatuses is inferred as type {
// readonly PENDING: "PENDING";
// readonly IN_PROGRESS: "IN_PROGRESS";
// readonly COMPLETED: "COMPLETED";
// readonly FAILED: "FAILED";
// }
ณ จุดนี้ JobStatuses เป็นออบเจกต์ JavaScript ที่รันไทม์ เหมือนกับ enum ทั่วไป อย่างไรก็ตาม การอนุมานชนิดข้อมูลของมันมีความแม่นยำมากกว่ามาก
การรวมกับ typeof และ keyof สำหรับ Union Types
พลังที่แท้จริงจะปรากฏขึ้นเมื่อเรารวม as const เข้ากับตัวดำเนินการ typeof และ keyof ของ TypeScript เพื่อสร้าง union type จากค่าหรือคีย์ของออบเจกต์
const JobStatuses = {
PENDING: "PENDING",
IN_PROGRESS: "IN_PROGRESS",
COMPLETED: "COMPLETED",
FAILED: "FAILED",
} as const;
// Type representing the keys (e.g., "PENDING" | "IN_PROGRESS" | ...)
type JobStatusKeys = keyof typeof JobStatuses;
// Type representing the values (e.g., "PENDING" | "IN_PROGRESS" | ...)
type JobStatusValues = typeof JobStatuses[keyof typeof JobStatuses];
function processJobWithConstAssertion(status: JobStatusValues): void {
if (status === JobStatuses.COMPLETED) {
console.log("Job finished successfully.");
} else if (status === JobStatuses.FAILED) {
console.log("Job encountered an error.");
} else {
console.log(`Job is currently ${status}.`);
}
}
let currentJobStatusFromObject: JobStatusValues = JobStatuses.IN_PROGRESS;
processJobWithConstAssertion(currentJobStatusFromObject);
// This would result in a compile-time error:
// let invalidStatusFromObject: JobStatusValues = "CANCELLED"; // Error!
รูปแบบนี้ให้สิ่งที่ดีที่สุดทั้งสองอย่าง: ออบเจกต์รันไทม์สำหรับการวนซ้ำหรือการเข้าถึงคุณสมบัติโดยตรง และ union type ที่คอมไพล์ไทม์สำหรับการตรวจสอบชนิดข้อมูลที่เข้มงวด
ประโยชน์ของ Const Assertions กับ Derived Union Types
- แหล่งความจริงเดียว: คุณกำหนดค่าคงที่ของคุณเพียงครั้งเดียวในออบเจกต์ JavaScript ธรรมดา และสร้างทั้งการเข้าถึงรันไทม์และชนิดข้อมูลที่คอมไพล์ไทม์จากมัน ซึ่งช่วยลดการทำซ้ำได้อย่างมาก และปรับปรุงการบำรุงรักษาในทีมพัฒนาที่หลากหลาย
- ความปลอดภัยของชนิดข้อมูล: เช่นเดียวกับ union types คุณจะได้รับความปลอดภัยของชนิดข้อมูลที่ยอดเยี่ยม ทำให้มั่นใจว่าใช้เฉพาะค่าที่กำหนดไว้ล่วงหน้าเท่านั้น
- ความสามารถในการวนซ้ำที่รันไทม์: เนื่องจาก
JobStatusesเป็นออบเจกต์ JavaScript มาตรฐาน คุณสามารถวนซ้ำคีย์หรือค่าของมันได้อย่างง่ายดาย โดยใช้วิธีการ JavaScript มาตรฐาน เช่นObject.keys(),Object.values(), หรือObject.entries()สิ่งนี้มีค่าอย่างยิ่งสำหรับ UI แบบไดนามิก (เช่น การเติมข้อมูล dropdowns) หรือการบันทึก - ข้อมูลที่เกี่ยวข้อง: รูปแบบนี้รองรับการเชื่อมโยงข้อมูลเพิ่มเติมกับสมาชิก "enum" แต่ละตัวได้อย่างเป็นธรรมชาติ
- ศักยภาพ Tree-Shaking ที่ดีขึ้น (เทียบกับ Enums): แม้ว่า
as constจะสร้างออบเจกต์รันไทม์ แต่มันเป็นออบเจกต์ JavaScript มาตรฐาน Bundler สมัยใหม่โดยทั่วไปมีประสิทธิภาพมากกว่าในการ tree-shake คุณสมบัติที่ไม่ได้ใช้ หรือแม้แต่ออบเจกต์ทั้งหมดหากไม่มีการอ้างอิง เทียบกับเอาต์พุตการคอมไพล์ enum ของ TypeScript อย่างไรก็ตาม หากออบเจกต์มีขนาดใหญ่และใช้เพียงไม่กี่คุณสมบัติ ออบเจกต์ทั้งหมดก็ยังอาจถูกรวมอยู่หากนำเข้าในลักษณะที่ป้องกันการ tree-shaking แบบละเอียด - ความยืดหยุ่น: คุณสามารถกำหนดค่าที่ไม่ใช่แค่สตริงหรือตัวเลข แต่เป็นออบเจกต์ที่ซับซ้อนมากขึ้นได้หากจำเป็น ทำให้เป็นรูปแบบที่ยืดหยุ่นสูง
const FileOperations = {
UPLOAD: {
label: "Upload File",
icon: "upload-icon.svg",
permission: "can_upload"
},
DOWNLOAD: {
label: "Download File",
icon: "download-icon.svg",
permission: "can_download"
},
DELETE: {
label: "Delete File",
icon: "delete-icon.svg",
permission: "can_delete"
},
} as const;
type FileOperationType = keyof typeof FileOperations; // "UPLOAD" | "DOWNLOAD" | "DELETE"
type FileOperationDetail = typeof FileOperations[keyof typeof FileOperations]; // { label: string; icon: string; permission: string; }
function performOperation(opType: FileOperationType) {
const details = FileOperations[opType];
console.log(`Performing: ${details.label} (Permission: ${details.permission})`);
}
performOperation("UPLOAD");
ข้อเสียของ Const Assertions
- การมีอยู่ของออบเจกต์รันไทม์: แตกต่างจาก pure union types วิธีนี้ยังคงสร้างออบเจกต์ JavaScript ที่รันไทม์ แม้ว่าจะเป็นออบเจกต์มาตรฐานและมักจะดีกว่าสำหรับ tree-shaking เมื่อเทียบกับ enums แต่ก็ไม่ได้ถูกลบออกไปทั้งหมด
- คำนิยามชนิดข้อมูลที่ยาวขึ้นเล็กน้อย: การสร้าง union type (
keyof typeof ...หรือtypeof ...[keyof typeof ...]) ต้องใช้ไวยากรณ์ที่มากกว่าการแสดงรายการ literals สำหรับ union type - ศักยภาพในการใช้งานผิด: หากไม่ได้ใช้อย่างระมัดระวัง ออบเจกต์
as constที่มีขนาดใหญ่มากก็ยังคงสามารถเพิ่มขนาดแพ็คเกจได้อย่างมาก หากเนื้อหาของมันไม่ถูก tree-shake อย่างมีประสิทธิภาพข้ามขอบเขตโมดูล
สำหรับสถานการณ์ที่คุณต้องการทั้งการตรวจสอบชนิดข้อมูลที่คอมไพล์ไทม์ที่แข็งแกร่งและการรวบรวมค่าที่รันไทม์ที่สามารถวนซ้ำได้หรือให้ข้อมูลที่เกี่ยวข้อง as const มักเป็นทางเลือกที่ได้รับความนิยมในหมู่นักพัฒนา TypeScript ทั่วโลก
การเปรียบเทียบทางเลือก: ควรใช้อะไรเมื่อไหร่?
การเลือกระหว่าง union types และ const assertions ขึ้นอยู่กับความต้องการเฉพาะของคุณเกี่ยวกับสถานะที่รันไทม์ ความสามารถในการวนซ้ำ และว่าคุณต้องการเชื่อมโยงข้อมูลเพิ่มเติมกับค่าคงที่ของคุณหรือไม่ มาดูปัจจัยในการตัดสินใจกัน
ความเรียบง่าย vs. ความแข็งแกร่ง
- Union Types: ให้ความเรียบง่ายสูงสุดเมื่อคุณต้องการเพียงชุดค่า string หรือ numeric ที่แตกต่างกันอย่างปลอดภัยด้านชนิดข้อมูล ณ คอมไพล์ไทม์ พวกมันเป็นทางเลือกที่เบาที่สุด
- Const Assertions: ให้รูปแบบที่แข็งแกร่งกว่าเมื่อคุณต้องการทั้งความปลอดภัยของชนิดข้อมูลที่คอมไพล์ไทม์และออบเจกต์รันไทม์ที่สามารถสอบถาม วนซ้ำ หรือขยายด้วยเมตาดาต้าเพิ่มเติมได้ การตั้งค่าเริ่มต้นจะยาวกว่าเล็กน้อย แต่ก็คุ้มค่ากับคุณสมบัติที่ได้รับ
สถานะที่รันไทม์ vs. คอมไพล์ไทม์
- Union Types: เป็นโครงสร้างที่ทำงานเฉพาะตอนคอมไพล์ไทม์เท่านั้น พวกมันไม่สร้างโค้ด JavaScript ใดๆ เลย นี่เหมาะอย่างยิ่งสำหรับแอปพลิเคชันที่การลดขนาดแพ็คเกจมีความสำคัญสูงสุด และค่าต่างๆ ก็เพียงพอแล้วโดยไม่จำเป็นต้องเข้าถึงในฐานะออบเจกต์ที่รันไทม์
- Const Assertions: สร้างออบเจกต์ JavaScript ธรรมดาที่รันไทม์ ออบเจกต์นี้สามารถเข้าถึงและใช้งานได้ในโค้ด JavaScript ของคุณ แม้ว่าจะเพิ่มขนาดแพ็คเกจ แต่โดยทั่วไปแล้วจะมีประสิทธิภาพมากกว่า TypeScript enums และเป็นตัวเลือกที่ดีกว่าสำหรับการ tree-shaking
ข้อกำหนดด้านความสามารถในการวนซ้ำ
- Union Types: ไม่มีวิธีโดยตรงในการวนซ้ำค่าที่เป็นไปได้ทั้งหมดที่รันไทม์ หากคุณต้องการเติมเมนูดรอปดาวน์หรือแสดงตัวเลือกทั้งหมด คุณจะต้องกำหนดอาร์เรย์แยกต่างหากสำหรับค่าเหล่านี้ ซึ่งอาจนำไปสู่การซ้ำซ้อนได้
- Const Assertions: โดดเด่นในด้านนี้ เนื่องจากคุณทำงานกับออบเจกต์ JavaScript มาตรฐาน คุณจึงสามารถใช้
Object.keys(),Object.values()หรือObject.entries()เพื่อรับอาร์เรย์ของคีย์ ค่า หรือคู่คีย์-ค่าตามลำดับได้อย่างง่ายดาย สิ่งนี้ทำให้มันเหมาะสำหรับ UI แบบไดนามิกหรือสถานการณ์ใดๆ ที่ต้องการการนับที่รันไทม์
const PaymentMethods = {
CREDIT_CARD: "Credit Card",
PAYPAL: "PayPal",
BANK_TRANSFER: "Bank Transfer",
} as const;
type PaymentMethodType = keyof typeof PaymentMethods;
// Get all keys (e.g., for internal logic)
const methodKeys = Object.keys(PaymentMethods) as PaymentMethodType[];
console.log(methodKeys); // ["CREDIT_CARD", "PAYPAL", "BANK_TRANSFER"]
// Get all values (e.g., for display in a dropdown)
const methodLabels = Object.values(PaymentMethods);
console.log(methodLabels); // ["Credit Card", "PayPal", "Bank Transfer"]
// Get key-value pairs (e.g., for mapping)
const methodEntries = Object.entries(PaymentMethods);
console.log(methodEntries); // [["CREDIT_CARD", "Credit Card"], ...]
ผลกระทบต่อ Tree-Shaking
- Union Types: มีคุณสมบัติ tree-shake ได้โดยธรรมชาติ เนื่องจากทำงานเฉพาะตอนคอมไพล์ไทม์เท่านั้น
- Const Assertions: แม้ว่าจะสร้างออบเจกต์รันไทม์ แต่ bundler สมัยใหม่มักจะสามารถ tree-shake คุณสมบัติที่ไม่ได้ใช้ของออบเจกต์นี้ได้อย่างมีประสิทธิภาพมากกว่าเมื่อเทียบกับออบเจกต์ enum ที่สร้างโดย TypeScript อย่างไรก็ตาม หากมีการนำเข้าและอ้างอิงออบเจกต์ทั้งหมด ก็มีแนวโน้มที่จะถูกรวมไว้ การออกแบบโมดูลที่ระมัดระวังสามารถช่วยได้
แนวทางปฏิบัติที่ดีที่สุดและแนวทางแบบผสมผสาน
มันไม่ใช่สถานการณ์แบบ "เลือกอย่างใดอย่างหนึ่ง" เสมอไป บ่อยครั้ง วิธีแก้ปัญหาที่ดีที่สุดเกี่ยวข้องกับการใช้แนวทางแบบผสมผสาน โดยเฉพาะอย่างยิ่งในแอปพลิเคชันขนาดใหญ่ที่รองรับหลายภาษา:
- สำหรับแฟล็กหรือตัวระบุภายในที่เรียบง่าย ซึ่งไม่จำเป็นต้องวนซ้ำหรือมีข้อมูลที่เกี่ยวข้องเลย Union Types โดยทั่วไปเป็นทางเลือกที่มีประสิทธิภาพและสะอาดที่สุด
- สำหรับชุดค่าคงที่ที่จำเป็นต้องวนซ้ำ แสดงผลใน UI หรือมีเมตาดาต้าที่เกี่ยวข้องที่หลากหลาย (เช่น ป้ายกำกับ ไอคอน หรือสิทธิ์) รูปแบบ Const Assertions จะเหนือกว่า
- การรวมกันเพื่อความสามารถในการอ่านและการรองรับหลายภาษา: หลายทีมใช้
as constสำหรับตัวระบุภายใน แล้วนำป้ายกำกับสำหรับแสดงผลที่แปลแล้วมาจากระบบการรองรับหลายภาษา (i18n) แยกต่างหาก
// src/constants/order-status.ts
const OrderStatuses = {
PENDING: "PENDING",
PROCESSING: "PROCESSING",
SHIPPED: "SHIPPED",
DELIVERED: "DELIVERED",
CANCELLED: "CANCELLED",
} as const;
type OrderStatus = typeof OrderStatuses[keyof typeof OrderStatuses];
export { OrderStatuses, type OrderStatus };
// src/i18n/en.json
{
"orderStatus": {
"PENDING": "Pending Confirmation",
"PROCESSING": "Processing Order",
"SHIPPED": "Shipped",
"DELIVERED": "Delivered",
"CANCELLED": "Cancelled"
}
}
// src/i18n/locales/es.json
{
"productCategories": {
"ELECTRONICS": "Electrónica",
"APPAREL": "Ropa y Accesorios",
"HOME_GOODS": "Artículos para el hogar",
"BOOKS": "Libros"
}
}
// src/components/OrderStatusDisplay.tsx
import { OrderStatuses, type OrderStatus } from "../constants/order-status";
import { useTranslation } from "react-i18next"; // Example i18n library
interface OrderStatusDisplayProps {
status: OrderStatus;
}
function OrderStatusDisplay({ status }: OrderStatusDisplayProps) {
const { t } = useTranslation();
const displayLabel = t(`orderStatus.${status}`);
return <span>Status: {displayLabel}</span>;
}
// Usage:
// <OrderStatusDisplay status={OrderStatuses.DELIVERED} />
แนวทางแบบผสมผสานนี้ใช้ประโยชน์จากความปลอดภัยของชนิดข้อมูลและความสามารถในการวนซ้ำที่รันไทม์ของ as const ในขณะที่ยังคงแยกสตริงสำหรับแสดงผลที่แปลแล้วออกจากกันและสามารถจัดการได้ ซึ่งเป็นข้อพิจารณาที่สำคัญสำหรับแอปพลิเคชันระดับโลก
รูปแบบขั้นสูงและข้อควรพิจารณา
นอกเหนือจากการใช้งานพื้นฐานแล้ว ทั้ง union types และ const assertions ยังสามารถรวมเข้ากับรูปแบบที่ซับซ้อนมากขึ้นเพื่อเพิ่มคุณภาพโค้ดและความสามารถในการบำรุงรักษา
การใช้ Type Guards กับ Union Types
เมื่อทำงานกับ union types โดยเฉพาะอย่างยิ่งเมื่อ union มีชนิดข้อมูลที่หลากหลาย (ไม่ใช่แค่ literals) type guards จะกลายเป็นสิ่งสำคัญสำหรับการจำกัดชนิดข้อมูลให้แคบลง ด้วย literal union types, discriminated unions มอบพลังที่มหาศาล
type SuccessEvent = { type: "SUCCESS"; data: any; };
type ErrorEvent = { type: "ERROR"; message: string; code: number; };
type SystemEvent = SuccessEvent | ErrorEvent;
function handleSystemEvent(event: SystemEvent) {
if (event.type === "SUCCESS") {
console.log("Data received:", event.data);
// event is now narrowed to SuccessEvent
} else {
console.log("Error occurred:", event.message, "Code:", event.code);
// event is now narrowed to ErrorEvent
}
}
handleSystemEvent({ type: "SUCCESS", data: { user: "Alice" } });
handleSystemEvent({ type: "ERROR", message: "Network failure", code: 503 });
รูปแบบนี้ซึ่งมักเรียกว่า "discriminated unions" มีความแข็งแกร่งและปลอดภัยด้านชนิดข้อมูลอย่างไม่น่าเชื่อ โดยให้การรับประกันที่คอมไพล์ไทม์เกี่ยวกับโครงสร้างข้อมูลของคุณ โดยอิงจากคุณสมบัติ literal ทั่วไป (discriminator)
Object.values() กับ as const และ Type Assertions
เมื่อใช้รูปแบบ as const, Object.values() อาจมีประโยชน์มาก อย่างไรก็ตาม การอนุมานค่าเริ่มต้นของ TypeScript สำหรับ Object.values() อาจกว้างกว่าที่ต้องการ (เช่น string[] แทนที่จะเป็น union ของ literals ที่เฉพาะเจาะจง) คุณอาจต้องใช้ type assertion เพื่อความเข้มงวด
const Statuses = {
ACTIVE: "Active",
INACTIVE: "Inactive",
PENDING: "Pending",
} as const;
type StatusValue = typeof Statuses[keyof typeof Statuses]; // "Active" | "Inactive" | "Pending"
// Object.values(Statuses) is inferred as (string | "Active" | "Inactive" | "Pending")[]
// We can assert it more narrowly if needed:
const allStatusValues: StatusValue[] = Object.values(Statuses);
console.log(allStatusValues); // ["Active", "Inactive", "Pending"]
// For a dropdown, you might pair values with labels if they differ
const statusOptions = Object.entries(Statuses).map(([key, value]) => ({
value: key, // Use the key as the actual identifier
label: value // Use the value as the display label
}));
console.log(statusOptions);
/*
[
{ value: "ACTIVE", label: "Active" },
{ value: "INACTIVE", label: "Inactive" },
{ value: "PENDING", label: "Pending" }
]
*/
สิ่งนี้แสดงให้เห็นถึงวิธีการรับอาร์เรย์ของค่าที่มีชนิดข้อมูลที่แข็งแกร่งซึ่งเหมาะสมสำหรับองค์ประกอบ UI ในขณะที่ยังคงรักษาชนิด literal ไว้
การรองรับหลายภาษา (i18n) และป้ายกำกับที่แปลแล้ว
สำหรับแอปพลิเคชันระดับโลก การจัดการสตริงที่แปลแล้วมีความสำคัญสูงสุด แม้ว่า TypeScript enums และทางเลือกของมันจะให้ตัวระบุภายใน แต่ป้ายกำกับสำหรับแสดงผลมักจะต้องถูกแยกออกสำหรับการรองรับหลายภาษา (i18n) รูปแบบ as const เข้ากันได้ดีกับระบบ i18n
คุณกำหนดตัวระบุภายในที่ไม่เปลี่ยนแปลงของคุณโดยใช้ as const ตัวระบุเหล่านี้จะสอดคล้องกันในทุกภาษา และทำหน้าที่เป็นคีย์สำหรับไฟล์การแปลของคุณ จากนั้นสตริงสำหรับแสดงผลจริงจะถูกดึงมาจากไลบรารี i18n (เช่น react-i18next, vue-i18n, FormatJS) โดยอิงตามภาษาที่ผู้ใช้เลือก
// app/features/product/constants.ts
export const ProductCategories = {
ELECTRONICS: "ELECTRONICS",
APPAREL: "APPAREL",
HOME_GOODS: "HOME_GOODS",
BOOKS: "BOOKS",
} as const;
export type ProductCategory = typeof ProductCategories[keyof typeof ProductCategories];
// app/i18n/locales/en.json
{
"productCategories": {
"ELECTRONICS": "Electronics",
"APPAREL": "Apparel & Accessories",
"HOME_GOODS": "Home Goods",
"BOOKS": "Books"
}
}
// app/i18n/locales/es.json
{
"productCategories": {
"ELECTRONICS": "Electrónica",
"APPAREL": "Ropa y Accesorios",
"HOME_GOODS": "Artículos para el hogar",
"BOOKS": "Libros"
}
}
// app/components/ProductCategorySelector.tsx
import { ProductCategories, type ProductCategory } from "../features/product/constants";
import { useTranslation } from "react-i18next";
function ProductCategorySelector() {
const { t } = useTranslation();
return (
<select>
{Object.values(ProductCategories).map(categoryKey => (
<option key={categoryKey} value={categoryKey}>
{t(`productCategories.${categoryKey}`)}
</option>
))}
</select>
);
}
การแยกส่วนความรับผิดชอบนี้มีความสำคัญอย่างยิ่งสำหรับแอปพลิเคชันระดับโลกที่ปรับขนาดได้ ชนิดข้อมูลของ TypeScript ช่วยให้คุณมั่นใจได้ว่าคุณใช้คีย์ที่ถูกต้องเสมอ และระบบ i18n จะจัดการเลเยอร์การนำเสนอตามภาษาที่ผู้ใช้เลือก สิ่งนี้ช่วยหลีกเลี่ยงการมีสตริงที่ขึ้นกับภาษาฝังอยู่ในตรรกะหลักของแอปพลิเคชันของคุณโดยตรง ซึ่งเป็น anti-pattern ทั่วไปสำหรับทีมงานระดับนานาชาติ
บทสรุป: เสริมสร้างทางเลือกการออกแบบ TypeScript ของคุณ
ในขณะที่ TypeScript ยังคงพัฒนาและเสริมสร้างศักยภาพให้นักพัฒนาทั่วโลกสามารถสร้างแอปพลิเคชันที่แข็งแกร่งและปรับขนาดได้มากขึ้น การทำความเข้าใจคุณสมบัติที่ละเอียดอ่อนและทางเลือกต่างๆ ก็มีความสำคัญมากขึ้นเรื่อยๆ แม้ว่าคำหลัก enum ของ TypeScript จะนำเสนอวิธีการที่สะดวกในการกำหนดค่าคงที่ที่มีชื่อ แต่ภาระที่รันไทม์ ข้อจำกัดในการ tree-shaking และความซับซ้อนของการ reverse mapping มักจะทำให้ทางเลือกที่ทันสมัยน่าสนใจยิ่งขึ้นสำหรับโปรเจกต์ที่คำนึงถึงประสิทธิภาพหรือโปรเจกต์ขนาดใหญ่
Union Types กับ String/Numeric Literals โดดเด่นในฐานะโซลูชันที่กระชับที่สุดและเน้นการทำงานที่คอมไพล์ไทม์เป็นหลัก พวกมันให้ความปลอดภัยของชนิดข้อมูลที่ไม่มีข้อบกพร่องโดยไม่สร้างโค้ด JavaScript ใดๆ ที่รันไทม์ ทำให้เหมาะสำหรับสถานการณ์ที่ขนาดแพ็คเกจขั้นต่ำและการ tree-shaking สูงสุดเป็นสิ่งสำคัญ และการนับที่รันไทม์ไม่ใช่ข้อกังวล
ในทางกลับกัน Const Assertions (as const) ที่รวมกับ typeof และ keyof นำเสนอรูปแบบที่ยืดหยุ่นและทรงพลังสูง พวกมันให้แหล่งความจริงเดียวสำหรับค่าคงที่ของคุณ ความปลอดภัยของชนิดข้อมูลที่แข็งแกร่งที่คอมไพล์ไทม์ และความสามารถที่สำคัญในการวนซ้ำค่าต่างๆ ที่รันไทม์ วิธีการนี้เหมาะอย่างยิ่งสำหรับสถานการณ์ที่คุณต้องการเชื่อมโยงข้อมูลเพิ่มเติมกับค่าคงที่ของคุณ เติมข้อมูลลงใน UI แบบไดนามิก หรือรวมเข้ากับระบบการรองรับหลายภาษาได้อย่างราบรื่น
ด้วยการพิจารณาข้อดีข้อเสียอย่างรอบคอบ ไม่ว่าจะเป็นภาระที่รันไทม์ ความจำเป็นในการวนซ้ำ และความซับซ้อนของข้อมูลที่เกี่ยวข้อง คุณสามารถตัดสินใจอย่างมีข้อมูลซึ่งนำไปสู่โค้ด TypeScript ที่สะอาดขึ้น มีประสิทธิภาพมากขึ้น และบำรุงรักษาง่ายขึ้น การยอมรับทางเลือกเหล่านี้ไม่ใช่แค่การเขียน TypeScript "สมัยใหม่" เท่านั้น แต่ยังเกี่ยวกับการตัดสินใจทางสถาปัตยกรรมอย่างตั้งใจที่ช่วยเพิ่มประสิทธิภาพแอปพลิเคชัน ประสบการณ์ของนักพัฒนา และความยั่งยืนในระยะยาวสำหรับผู้ชมทั่วโลก
เสริมสร้างการพัฒนา TypeScript ของคุณด้วยการเลือกเครื่องมือที่เหมาะสมกับงาน ย้ายออกจาก enum เริ่มต้นเมื่อมีทางเลือกที่ดีกว่า